package server;

import java.beans.XMLDecoder;
import java.beans.XMLEncoder;
import java.io.*;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.rmi.*;
import java.rmi.registry.*;
import java.rmi.server.UnicastRemoteObject;
import java.util.*;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.logging.*;

import common.*;
import static common.Constants.*;
import static common.NotificationMsg.*;

public class IMServer extends UnicastRemoteObject implements ServerInterface,
		java.io.Serializable {

	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;
	private static final String F_SEP = System.getProperty("file.separator");
	private static final String IM_HOME = getServerHome();

	private static final String SERVER_CONF = IM_HOME + "conf" + F_SEP
			+ "server.xml";
	private static final String SECURITY_FILE = IM_HOME + "conf" + F_SEP
			+ "security.policy";
	private static final String USER_DATA = IM_HOME + "data" + F_SEP
			+ "user.xml";
	private static final String KEY_DATA = IM_HOME + "data" + F_SEP + "key.xml";
	private static final String MESSAGE_DATA = IM_HOME + "data" + F_SEP
			+ "message.xml";
	private static final String LOG_FILE = IM_HOME + "log" + F_SEP
			+ "server.log";

	private static IMServer server;
	private static String codeBase;
	private static ServerConfiguration config;
	private static Logger log;
	private static FileHandler logHandler;

	private HashMap<Long, User> userList;
	private HashMap<Long, String> userKey;
	private HashMap<Long, ArrayList<Message>> messages;
	private HashMap<Long, ArrayBlockingQueue<NotificationMsg>> notifications;
	private HashMap<Long, String> clients;
	private HashMap<Long, Long> lastActTimes;
	private HashMap<Long, String[]> fontSupports;
	private Thread userCleaner;

	private IMServer() throws RemoteException {
		super(config.getServerPort());
		userList = new HashMap<Long, User>();
		userKey = new HashMap<Long, String>();
		messages = new HashMap<Long, ArrayList<Message>>();
		clients = new HashMap<Long, String>();
		lastActTimes = new HashMap<Long, Long>();
		fontSupports = new HashMap<Long, String[]>();
		notifications = new HashMap<Long, ArrayBlockingQueue<NotificationMsg>>();
		loadUserData();
		IdGenerator.init(config.getLastUID());
		userCleaner = new Thread(new InactiveUserCleaner(TimeUnit.SECONDS
				.toMillis(45)));
		userCleaner.start();
	}

	private static String getServerHome() {
		String home = System.getenv("IM_SERVER_HOME");
		if(home == null) home = System.getProperty("im.server.home");
		if (home == null)
			return new File("").getAbsolutePath();
		return home + F_SEP;
	}

	private static void initLogging() {
		log = Logger.getLogger("RMI IMI Server log");
		try {
			logHandler = new FileHandler(LOG_FILE);
			log.addHandler(logHandler);
			log.setLevel(Level.ALL);
			log.setUseParentHandlers(false);
			logHandler.setFormatter(new SimpleFormatter());
		} catch (SecurityException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	@SuppressWarnings("unchecked")
	private void loadUserData() {
		File userData = new File(USER_DATA);
		File messageData = new File(MESSAGE_DATA);
		File keyData = new File(KEY_DATA);
		try {
			if (userData.exists()) {
				XMLDecoder decoder = new XMLDecoder(new BufferedInputStream(
						new FileInputStream(userData)));
				userList = (HashMap<Long, User>) decoder.readObject();
				decoder.close();
				for (User u : userList.values()) {
					u.setStatus(User.STATUS_OFFLINE);
				}
				log.info(userList.size() + " users loaded");
			}
			if (keyData.exists()) {
				XMLDecoder decoder = new XMLDecoder(new BufferedInputStream(
						new FileInputStream(keyData)));
				userKey = (HashMap<Long, String>) decoder.readObject();
				decoder.close();
			}
			if (messageData.exists()) {
				XMLDecoder decoder = new XMLDecoder(new BufferedInputStream(
						new FileInputStream(messageData)));
				messages = (HashMap<Long, ArrayList<Message>>) decoder
						.readObject();
				decoder.close();
				log.info(messages.size() + " users's message loaded");
			}
		} catch (IOException e) {
			log.severe("cannot load data file");
		}

	}

	private void saveUserData() {
		log.info("Saving user data to file");
		try {
			XMLEncoder encoder = new XMLEncoder(new BufferedOutputStream(
					new FileOutputStream(USER_DATA)));
			encoder.writeObject(userList);
			encoder.close();
			encoder = new XMLEncoder(new BufferedOutputStream(
					new FileOutputStream(KEY_DATA)));
			encoder.writeObject(userKey);
			encoder.close();
			log.info(userList.size() + " users saved");
			encoder = new XMLEncoder(new BufferedOutputStream(
					new FileOutputStream(MESSAGE_DATA)));
			encoder.writeObject(messages);
			encoder.close();
			log.info(messages.size() + " users's message saved");
		} catch (IOException e) {
			log.severe("Cannot save user data");
		}

	}

	private static void loadConfig() {
		log.config("Loading configuration from " + SERVER_CONF);
		File serverConf = new File(SERVER_CONF);
		try {
			if (serverConf.exists()) {
				XMLDecoder decoder = new XMLDecoder(new BufferedInputStream(
						new FileInputStream(serverConf)));
				config = (ServerConfiguration) decoder.readObject();
				decoder.close();
			} else {
				config = new ServerConfiguration();
				config.setServerPort(SERVER_PORT);
				config.setServerServiceName(RMI_SERVICE_NAME);
				config.setSecurityFile(SECURITY_FILE);
				config.setKey(java.util.UUID.randomUUID().toString());
				config.setLastUID(1);
				String hostname = SERVER_ADDRESS;
				try {
					InetAddress local = InetAddress.getLocalHost();
					hostname = local.getCanonicalHostName();
				} catch (UnknownHostException e) {
					log.info("Cannot get localhost");
				}
				config.setServerPublicHostname(hostname);
				XMLEncoder encoder = new XMLEncoder(new BufferedOutputStream(
						new FileOutputStream(serverConf)));
				encoder.writeObject(config);
				encoder.close();
			}
			if(codeBase!=null)
				config.setCodeBase(codeBase);

		} catch (FileNotFoundException e) {
			log.severe("cannot found config file");
		}
	}

	private static void saveConfig() {
		log.info("Saving server configuration");
		try {
			XMLEncoder encoder = new XMLEncoder(new BufferedOutputStream(
					new FileOutputStream(SERVER_CONF)));
			config.setLastUID(IdGenerator.next()-1);
			encoder.writeObject(config);
			encoder.close();

		} catch (FileNotFoundException e) {
			log.severe("Cannot save config");
		}
	}

	public static synchronized IMServer getIMServer() throws RemoteException {
		if (server == null) {
			server = new IMServer();
		}
		return server;
	}

	public static void startServer() {
		loadConfig();
		init();
		try {
			Registry registry = LocateRegistry.getRegistry(config
					.getServerPort());
			IMServer server = IMServer.getIMServer();
			registry.rebind(config.getServerServiceName(), server);
		} catch (AccessException e) {
			log.severe(e.getMessage());
			System.exit(0);
		} catch (RemoteException e) {
			log.severe(e.getMessage());
			System.exit(0);
		}
		log.info("Server started");
		saveConfig();
	}

	public static void init() {
		// it's required for RMI, which defines the security policy for the
		// server
		log.config("loading security file " + config.getSecurityFile());
		System.setProperty("java.security.policy", config.getSecurityFile());
		// Required by RMI, used to find the server's java class
		log.config("set code base to " + config.getCodeBase());
		System.setProperty("java.rmi.server.codebase", config.getCodeBase());
		System.setProperty("java.rmi.server.hostname", config.getServerPublicHostname());
		// if there is not security manager, create one.
		if (System.getSecurityManager() == null) {
			System.setSecurityManager(new SecurityManager());
		}
		try {
			// Try to create a new registry.
			LocateRegistry.createRegistry(config.getServerPort());
			log
					.config("RMI Registry created on port:"
							+ config.getServerPort());
		} catch (RemoteException e) {
			// There is a running Registry on that port, don't need
			// to create another one, just need to bind the agent
			log.config("RMI registry already running, try to use exist one");
		}
	}

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		initLogging();
		if (args.length >= 1) {
			codeBase = new File(args[0]).toURI().toString();
		}
		IMServer.startServer();

	}

	private class ClientNotifier implements Runnable {
		private final Long id;
		private final NotificationMsg msg;

		public ClientNotifier(Long id, NotificationMsg msg) {
			this.id = id;
			this.msg = msg;
		}

		@Override
		public void run() {
			if (notifications.get(id) != null)
				notifications.get(id).add(msg);
		}
	}

	private class InactiveUserCleaner implements Runnable {
		private final long timeout;

		public InactiveUserCleaner(long timeout) {
			this.timeout = timeout;
		}

		@Override
		public void run() {
			while (!Thread.interrupted()) {
				synchronized (lastActTimes) {
					for (long id : Collections
							.unmodifiableCollection(lastActTimes.keySet())) {
						if (System.currentTimeMillis() - lastActTimes.get(id) > timeout) {
							lastActTimes.remove(id);
							userList.get(id).setStatus(User.STATUS_OFFLINE);
							notifications.remove(id);
							notifyAllClients(new NotificationMsg(
									USERSTATUS_CHANGE, id));
						}
					}
				}
				try {
					Thread.sleep(timeout);
				} catch (InterruptedException e) {
					return;
				}
			}
		}
	}

	private static class IdGenerator {
		private static long id;

		public static synchronized void init(long start) {
			id = start;
		}

		public static synchronized long next() {
			return id++;
		}
	}

	@Override
	public List<Message> getMessage(long clientId, String key)
			throws RemoteException {
		if (clients.get(clientId).equals(key))
			try {
				return messages.get(clientId);
			} finally {
				messages.put(clientId, new ArrayList<Message>());
			}
		else
			return new ArrayList<Message>();
	}

	private void notifyAllClients(NotificationMsg msg) {
		synchronized (lastActTimes) {
			for (Long id : lastActTimes.keySet()) {
				new Thread(new ClientNotifier(id, msg)).start();
			}
		}
	}

	@Override
	public void logout(long clientId, String key) throws RemoteException {
		User u = userList.get(clientId);
		if (u == null || !clients.get(u.getUserId()).equals(key))
			return;
		u.setStatus(User.STATUS_OFFLINE);
		lastActTimes.remove(u.getUserId());
		notifications.remove(u.getUserId());
		log.info(u + " logged out");
		notifyAllClients(new NotificationMsg(USERSTATUS_CHANGE, clientId));
	}

	@Override
	public boolean SendMessage(long receiverId, Message message, String key)
			throws RemoteException {
		if (message == null || !userKey.get(message.getSenderId()).equals(key))
			return true;
		try {
			if (userList.get(receiverId) == null)
				return false;
			if (messages.get(receiverId) == null)
				messages.put(receiverId, new ArrayList<Message>());
			messages.get(receiverId).add(message);
			log
					.info(message.getSenderId() + " send a message to "
							+ receiverId);
			return true;
		} finally {
			if (userList.get(receiverId).getStatus() != User.STATUS_OFFLINE)
				new Thread(new ClientNotifier(receiverId, new NotificationMsg(
						NEW_MESSAGE, null))).start();
		}
	}

	@Override
	public Collection<User> getUsers() throws RemoteException {
		return new ArrayList<User>(userList.values());
	}

	@Override
	public void shutDown(String id) throws RemoteException {
		if (id.equals(config.getKey())) {
			log.info("Received Shutdown Signal, Shutting Down the server.");
			notifyAllClients(new NotificationMsg(SHUT_DOWN, null));
			userCleaner.interrupt();
			saveUserData();
			saveConfig();
			logHandler.close();
			try {
				Registry registry = LocateRegistry.getRegistry(config
						.getServerPort());
				registry.unbind(config.getServerServiceName());
			} catch (NotBoundException e) {
			}
			UnicastRemoteObject.unexportObject(server, true);
		}
	}

	@Override
	public PrivateUser login(long id, String alias, String key)
			throws RemoteException {
		User u = userList.get(id);
		if (u == null || !u.getAlias().equals(alias)
				|| !userKey.get(u.getUserId()).equals(key)) {
			u = new User(IdGenerator.next());
			userKey.put(u.getUserId(), UUID.randomUUID().toString());
			u.setAlias(alias);
			log.info("new user " + u + " was added");
		}
		userList.put(u.getUserId(), u);
		if (messages.get(u.getUserId()) == null)
			messages.put(u.getUserId(), new ArrayList<Message>());
		log.info(u + " logged in");
		clients.put(u.getUserId(), UUID.randomUUID().toString());
		if (u.getStatus() == User.STATUS_ONLINE)
			notifications.get(u.getUserId()).add(
					new NotificationMsg(SHUT_DOWN, null));
		notifications.put(u.getUserId(),
				new ArrayBlockingQueue<NotificationMsg>(30));
		notifications.get(u.getUserId()).add(
				new NotificationMsg(NEW_USERLIST, null));
		if (messages.get(u.getUserId()).size() > 0)
			notifications.get(u.getUserId()).add(
					new NotificationMsg(NEW_MESSAGE, null));
		u.setStatus(User.STATUS_ONLINE);
		PrivateUser pu = new PrivateUser(u);
		pu.setKey(userKey.get(u.getUserId()));
		pu.setClientId(clients.get(u.getUserId()));
		return pu;
	}

	@Override
	public User getUserById(long id) throws RemoteException {
		return userList.get(id);
	}

	@Override
	public NotificationMsg getNotification(long id, String key)
			throws RemoteException {
		if (clients.get(id).equals(key))
			try {
				if (lastActTimes.get(id) == null) {
					lastActTimes.put(id, System.currentTimeMillis());
					notifyAllClients(new NotificationMsg(USERSTATUS_CHANGE, id));
				}
				lastActTimes.put(id, System.currentTimeMillis());
				if(notifications.get(id) == null) return null;
				return notifications.get(id).poll(30, TimeUnit.SECONDS);
			} catch (InterruptedException e) {
				return null;
			}
		else
			return new NotificationMsg(SHUT_DOWN, null);
	}

	@Override
	public boolean changeUserInfo(PrivateUser puser) throws RemoteException {
		if (userKey.get(puser.getUserId()).equals(puser.getKey())
				&& clients.get(puser.getUserId()).equals(puser.getClientId())) {
			log.info(userList.get(puser.getUserId()) + " change it's alias to "
					+ puser.getAlias());
			userList.put(puser.getUserId(), puser.getUser());
			notifyAllClients(new NotificationMsg(USERSTATUS_CHANGE, puser
					.getUserId()));
			return true;
		}
		return false;
	}

	@Override
	public String[] getSupportFonts(long id) throws RemoteException {
		return fontSupports.get(id);
	}

	@Override
	public void setSupportFonts(long id, String key, String[] fontNames)
			throws RemoteException {
		if(clients.get(id).equals(key)){
			fontSupports.put(id, fontNames);
		}
	}

}
